﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using JelleDruyts.Windows;
using Microsoft.VisualBasic.ApplicationServices;
using Schedulr.Infrastructure;
using Schedulr.Messages;
using Schedulr.Models;
using Schedulr.Providers;

namespace Schedulr
{
    /// <summary>
    /// The entry point for the application.
    /// </summary>
    /// <remarks>
    /// This class uses the <see cref="WindowsFormsApplicationBase"/> to provide a single instance application.
    /// </remarks>
    public sealed class Program : WindowsFormsApplicationBase
    {
        #region Constants

        /// <summary>
        /// Gets the command-line argument prefix.
        /// </summary>
        public const string ArgumentPrefix = "/";

        /// <summary>
        /// Gets the command-line argument name to request help.
        /// </summary>
        public const string ArgumentNameHelp = ArgumentPrefix + "?";

        /// <summary>
        /// Gets the command-line argument name to request help (alternative).
        /// </summary>
        public const string ArgumentNameHelpAlt = ArgumentPrefix + "help";

        /// <summary>
        /// Gets the command-line argument key name to specify an account.
        /// </summary>
        public const string ArgumentKeyAccount = "account";

        /// <summary>
        /// Gets the command-line argument name to specify an account.
        /// </summary>
        public const string ArgumentNameAccount = ArgumentPrefix + ArgumentKeyAccount;

        /// <summary>
        /// Gets the command-line argument name to start an upload in the background.
        /// </summary>
        public const string ArgumentNameUpload = ArgumentPrefix + "upload";

        /// <summary>
        /// Gets the command-line argument name to start an upload through the user interface.
        /// </summary>
        public const string ArgumentNameUploadUI = ArgumentPrefix + "uploadui";

        /// <summary>
        /// Gets the command-line argument name to add pictures as individual batches.
        /// </summary>
        public const string ArgumentNameAdd = ArgumentPrefix + "add";

        /// <summary>
        /// Gets the command-line argument name to add pictures as a single batch.
        /// </summary>
        public const string ArgumentNameAddBatch = ArgumentPrefix + "addbatch";

        #endregion

        #region Fields

        /// <summary>
        /// A value that determines if the application is running interactively or in the background.
        /// </summary>
        private bool isInteractive;

        /// <summary>
        /// The application instance if it is running interactively.
        /// </summary>
        private App app;

        /// <summary>
        /// The configuration being used by the current application.
        /// </summary>
        private SchedulrConfiguration configuration;

        #endregion

        #region Main

        [STAThread]
        public static void Main(string[] args)
        {
            var manager = new Program();
            manager.IsSingleInstance = true;
            manager.Run(args);
        }

        #endregion

        #region Startup

        /// <summary>
        /// Raises the <see cref="E:Startup"/> event.
        /// </summary>
        /// <param name="e">The <see cref="Microsoft.VisualBasic.ApplicationServices.StartupEventArgs"/> instance containing the event data.</param>
        /// <returns></returns>
        protected override bool OnStartup(Microsoft.VisualBasic.ApplicationServices.StartupEventArgs e)
        {
            // Initialize.
            LogStartup(string.Format(CultureInfo.CurrentCulture, "Application {0} started ({1})", App.DisplayVersion, App.FullVersion.ToString()), e.CommandLine);

            // Catch unhandled exceptions on any thread.
            AppDomain.CurrentDomain.UnhandledException += delegate(object sender, System.UnhandledExceptionEventArgs args)
            {
                // Log the exception and show a message to the user. The application will terminate no matter what.
                var exception = args.ExceptionObject as Exception;
                Logger.Log("An unhandled exception occurred", exception);
                var message = new StringBuilder();
                message.Append("An unexpected exception occurred and the application will need to close. We apologize for the inconvenience.").AppendLine().AppendLine();
                message.AppendFormat(CultureInfo.CurrentCulture, "Please contact us at {0} and provide as much details as possible so we can fix the issue.", App.ApplicationUrl);
                if (exception != null)
                {
                    message.AppendLine().AppendLine();
                    message.Append("Exception message (more details are available in the log file):").AppendLine();
                    message.Append(exception.Message);
                }
                MessageBox.Show(App.Current.MainWindow, message.ToString(), "An unexpected exception occurred", MessageBoxButton.OK, MessageBoxImage.Error);
            };

            // Load the configuration.
            this.configuration = SchedulrConfigurationProvider.LoadOrCreate();

            // Start the application.
            ApplicationInstanceRequested(e.CommandLine.ToArray(), true);

            Logger.Log("Application exited", TraceEventType.Information);

            // Return false to indicate that startup should not continue.
            return false;
        }

        #endregion

        #region Startup Next Instance

        /// <summary>
        /// When overridden in a derived class, allows for code to run when a subsequent instance of a single-instance application starts.
        /// </summary>
        /// <param name="e"><see cref="T:Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs"/>. Contains the command-line arguments of the subsequent application instance and indicates whether the first application instance should be brought to the foreground upon exiting the exception handler.</param>
        protected override void OnStartupNextInstance(StartupNextInstanceEventArgs e)
        {
            base.OnStartupNextInstance(e);
            LogStartup("A new application instance was requested", e.CommandLine);

            // Activate the main window if available to make bring the application back to the foreground.
            if (this.app != null && this.app.MainWindow != null)
            {
                this.app.MainWindow.Activate();
            }

            // Pass the new instance request to the already running application.
            ApplicationInstanceRequested(e.CommandLine.ToArray(), false);
        }

        #endregion

        #region Application Instance Requested

        /// <summary>
        /// Handles a new application instance request.
        /// </summary>
        /// <param name="arguments">The command-line arguments.</param>
        /// <param name="firstInstance">Determines if this is the first instance or if the application is already running.</param>
        private void ApplicationInstanceRequested(string[] arguments, bool firstInstance)
        {
            CommandLineActions requestedActions;
            Account account;
            IList<string> fileNames;
            var shouldContinue = ParseArguments(arguments, firstInstance, out account, out requestedActions, out fileNames);
            if (!shouldContinue)
            {
                return;
            }

            if (requestedActions.HasFlag(CommandLineActions.Add) || requestedActions.HasFlag(CommandLineActions.AddBatch))
            {
                if (fileNames.Count == 0)
                {
                    Logger.Log("Pictures were requested to be added but there were no files to add", TraceEventType.Warning);
                }
                else
                {
                    Logger.Log("Add pictures requested for account " + account.Name, TraceEventType.Information);
                    var addToSingleBatch = requestedActions.HasFlag(CommandLineActions.AddBatch);
                    Tasks.AddPicturesToQueue(account, fileNames, null, addToSingleBatch);
                }
            }

            if (firstInstance)
            {
                if (requestedActions.HasFlag(CommandLineActions.UploadBackground))
                {
                    Logger.Log("Upload requested for account " + account.Name, TraceEventType.Information);

                    // Perform the upload in the background.
                    this.isInteractive = false;

                    // Upload the batch and at the end save changes to the configuration file.
                    var tempApp = new App();
                    var uploading = Tasks.UploadPictures(account, account.QueuedPictures.GetLowestBatch(), () =>
                    {
                        SchedulrConfigurationProvider.Save(this.configuration);
                        tempApp.Dispatcher.InvokeShutdown();
                    });

                    // In the mean time run a new application so the message pump is running and
                    // new application instance requests can be processed while uploading.
                    if (uploading)
                    {
                        tempApp.Run();
                    }
                }
                else
                {
                    // Start the application interactively.
                    this.isInteractive = true;
                    this.app = new App();
                    this.app.Initialize(this.configuration, account, requestedActions.HasFlag(CommandLineActions.UploadUI));
                    this.app.Run();
                }
            }
            else
            {
                if (this.isInteractive)
                {
                    // The application is already running interactively, perform the requested action or do nothing if the application was started normally.
                    if (requestedActions.HasFlag(CommandLineActions.UploadBackground) || requestedActions.HasFlag(CommandLineActions.UploadUI))
                    {
                        Logger.Log("Upload requested for account " + account.Name, TraceEventType.Information);
                        var reason = (requestedActions.HasFlag(CommandLineActions.UploadBackground) ? UploadPicturesRequestReason.CommandLineBackground : UploadPicturesRequestReason.CommandLineUI);
                        Messenger.Send<UploadPicturesRequestedMessage>(new UploadPicturesRequestedMessage(account.QueuedPictures.GetLowestBatch(), account, reason));
                    }
                }
                else
                {
                    // An upload is already running; show a message box and exit.
                    var message = string.Format(CultureInfo.CurrentCulture, "{1} is currently uploading pictures in the background so you cannot start the application at this time.{0}{0}Please try again after the upload has completed. You can monitor progress by looking at the log file at the following location:{0}{0}{2}", Environment.NewLine, App.ApplicationName, PathProvider.LogFilePath);
                    MessageBox.Show(message, App.ApplicationName, MessageBoxButton.OK, MessageBoxImage.Warning);
                }
            }
        }

        #endregion

        #region Helper Methods

        /// <summary>
        /// Parses the command-line arguments.
        /// </summary>
        /// <param name="arguments">The arguments.</param>
        /// <param name="firstInstance">Determines if this is the first instance of the application.</param>
        /// <param name="account">Upon return, contains the selected account.</param>
        /// <param name="requestedActions">Upon return, contains the requested command-line actions.</param>
        /// <param name="fileNames">Upon return, contains any file names passed for the requested actions.</param>
        /// <returns><see langword="true"/> if the command-line arguments were valid and the application should continue, <see langword="false"/> otherwise.</returns>
        private bool ParseArguments(string[] arguments, bool firstInstance, out Account account, out CommandLineActions requestedActions, out IList<string> fileNames)
        {
            // Parse the command-line arguments.
            IDictionary<string, string> nameValueArguments;
            IList<string> valueArguments;
            ParseArguments(arguments, out nameValueArguments, out valueArguments);

            // Determine the selected account to use.
            if (nameValueArguments.ContainsKey(Program.ArgumentKeyAccount.ToLowerInvariant()))
            {
                // An account name was specified, look up the account in the configuration.
                var accountName = GetArgument(nameValueArguments, Program.ArgumentKeyAccount);
                account = this.configuration.Accounts.Where(a => string.Equals(a.Name, accountName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
            }
            else
            {
                // No account was specified, use the default account.
                account = this.configuration.Accounts.Where(a => a.IsDefaultAccount).FirstOrDefault();
                if (account == null && this.configuration.Accounts.Count > 0)
                {
                    account = this.configuration.Accounts[0];
                }
            }

            // Determine the available file names.
            fileNames = new List<string>();
            foreach (var potentialFileName in valueArguments)
            {
                if (File.Exists(potentialFileName))
                {
                    fileNames.Add(potentialFileName);
                }
            }

            // Determine the actions to execute.
            var shouldContinue = true;
            var showHelp = false;
            string errorMessage = null;
            requestedActions = CommandLineActions.None;
            if (valueArguments.Contains(Program.ArgumentNameHelp, StringComparer.OrdinalIgnoreCase) || valueArguments.Contains(Program.ArgumentNameHelpAlt, StringComparer.OrdinalIgnoreCase))
            {
                showHelp = true;
            }
            else
            {
                if (valueArguments.Contains(Program.ArgumentNameUpload, StringComparer.OrdinalIgnoreCase))
                {
                    // An upload is requested, check the account.
                    requestedActions |= CommandLineActions.UploadBackground;
                    if (account == null)
                    {
                        showHelp = true;
                        errorMessage = "An upload was requested but the specified account was not found.";
                    }
                    else
                    {
                        if (account.QueuedPictures.Count == 0)
                        {
                            Logger.Log("An upload was requested but the account had no pictures to upload.", TraceEventType.Information);
                            shouldContinue = false;
                        }
                    }
                }
                if (valueArguments.Contains(Program.ArgumentNameUploadUI, StringComparer.OrdinalIgnoreCase) && account != null)
                {
                    requestedActions |= CommandLineActions.UploadUI;
                }
                if (valueArguments.Contains(Program.ArgumentNameAddBatch))
                {
                    requestedActions |= CommandLineActions.AddBatch;
                    if (account == null)
                    {
                        errorMessage = "Pictures were requested to be added to the queue but the specified account was not found.";
                        showHelp = true;
                    }
                }
                if (valueArguments.Contains(Program.ArgumentNameAdd) || (requestedActions == CommandLineActions.None && fileNames.Count > 0))
                {
                    // If no action is requested explicitly and there are files in the arguments, assume they should be added.
                    requestedActions |= CommandLineActions.Add;
                    if (account == null)
                    {
                        errorMessage = "Pictures were requested to be added to the queue but the specified account was not found.";
                        showHelp = true;
                    }
                }
            }

            if (showHelp)
            {
                // Incorrect parameters were passed, show a usage message and exit.
                shouldContinue = false;
                var usageMessage = new StringBuilder();
                var icon = MessageBoxImage.Information;
                if (!string.IsNullOrEmpty(errorMessage))
                {
                    usageMessage.AppendFormat(CultureInfo.CurrentCulture, "You have started {0} with incorrect command-line parameters. {1}", App.ApplicationName, errorMessage).AppendLine().AppendLine();
                    icon = MessageBoxImage.Error;
                    Logger.Log("Incorrect command-line parameters. " + errorMessage, TraceEventType.Error);
                }
                usageMessage.Append("Usage:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} [{1}:name] [{2}] [{3} | {4}] [file1 file2...]", App.ExecutableName, Program.ArgumentNameAccount, Program.ArgumentNameUpload, Program.ArgumentNameAdd, Program.ArgumentNameAddBatch).AppendLine().AppendLine();
                usageMessage.Append("Examples:").AppendLine().AppendLine();
                usageMessage.Append("Upload the next batch of pictures from the default account:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} {1}", App.ExecutableName, Program.ArgumentNameUpload).AppendLine().AppendLine();
                usageMessage.Append("Upload the next batch of pictures from a specified account:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} {1} {2}:\"My Account\"", App.ExecutableName, Program.ArgumentNameUpload, Program.ArgumentNameAccount).AppendLine().AppendLine();
                usageMessage.Append("Add pictures to the queue of the default account:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} \"My Picture 1.jpg\" \"My Picture 2.jpg\"", App.ExecutableName).AppendLine().AppendLine();
                usageMessage.Append("Add pictures to the queue of a specified account:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} {1} {2}:\"My Account\" \"My Picture 1.jpg\" \"My Picture 2.jpg\"", App.ExecutableName, Program.ArgumentNameAdd, Program.ArgumentNameAccount).AppendLine().AppendLine();
                usageMessage.Append("Add a single batch of pictures to the queue of the default account:").AppendLine();
                usageMessage.AppendFormat(CultureInfo.CurrentCulture, "    {0} {1} \"My Picture 1.jpg\" \"My Picture 2.jpg\"", App.ExecutableName, Program.ArgumentNameAddBatch);
                MessageBox.Show(usageMessage.ToString(), App.ApplicationName, MessageBoxButton.OK, icon);
            }

            return shouldContinue;
        }

        private static void ParseArguments(string[] args, out IDictionary<string, string> nameValueArguments, out IList<string> valueArguments)
        {
            nameValueArguments = new Dictionary<string, string>();
            valueArguments = new List<string>();
            foreach (var arg in args)
            {
                // Parse arguments in the form "/name:value".
                if (arg != null && arg.StartsWith(ArgumentPrefix) && arg.IndexOf(":") >= 2)
                {
                    var separatorIndex = arg.IndexOf(":");
                    var argumentName = arg.Substring(1, separatorIndex - 1);
                    var argumentValue = arg.Substring(separatorIndex + 1);
                    nameValueArguments.Add(argumentName.ToLowerInvariant(), argumentValue);
                }
                else
                {
                    valueArguments.Add(arg);
                }
            }
        }

        private static string GetArgument(IDictionary<string, string> arguments, params string[] argumentNames)
        {
            foreach (var argumentName in argumentNames)
            {
                if (arguments.ContainsKey(argumentName.ToLowerInvariant()))
                {
                    return arguments[argumentName.ToLowerInvariant()];
                }
            }
            return null;
        }

        /// <summary>
        /// Logs application startup (either the first instance or a subsequent instance).
        /// </summary>
        /// <param name="startupMessage">The startup message.</param>
        /// <param name="arguments">The arguments.</param>
        private static void LogStartup(string startupMessage, ICollection<string> arguments)
        {
            Logger.Log(startupMessage, TraceEventType.Information);
            if (arguments != null && arguments.Count > 0)
            {
                var argumentsMessage = new StringBuilder("Command-line parameters: ");
                foreach (var argument in arguments)
                {
                    if (argument.Contains(' '))
                    {
                        // Enclose arguments with spaces in quotes for display purposes.
                        argumentsMessage.AppendFormat(CultureInfo.CurrentCulture, "\"{0}\"", argument);
                    }
                    else
                    {
                        argumentsMessage.Append(argument);
                    }
                    argumentsMessage.Append(" ");
                }
                Logger.Log(argumentsMessage.ToString().Trim(), TraceEventType.Information);
            }
        }

        #endregion

        #region GetCommandLineArgumentsForUpload

        /// <summary>
        /// Gets the command line arguments for uploading pictures from a specified account in the background.
        /// </summary>
        /// <param name="accountName">The account name.</param>
        /// <returns>The command line arguments to use to upload pictures from the specified account.</returns>
        public static string GetCommandLineArgumentsForUploadBackground(string accountName)
        {
            return GetCommandLineArgumentsForUpload(Program.ArgumentNameUpload, accountName);
        }

        /// <summary>
        /// Gets the command line arguments for uploading pictures from a specified account through the user interface.
        /// </summary>
        /// <param name="accountName">The account name.</param>
        /// <returns>The command line arguments to use to upload pictures from the specified account.</returns>
        public static string GetCommandLineArgumentsForUploadUI(string accountName)
        {
            return GetCommandLineArgumentsForUpload(Program.ArgumentNameUploadUI, accountName);
        }

        /// <summary>
        /// Gets the command line arguments for uploading pictures from a specified account.
        /// </summary>
        /// <param name="uploadArgument">The upload argument.</param>
        /// <param name="accountName">The account name.</param>
        /// <returns>The command line arguments to use to upload pictures from the specified account.</returns>
        private static string GetCommandLineArgumentsForUpload(string uploadArgument, string accountName)
        {
            if (!string.IsNullOrEmpty(accountName))
            {
                uploadArgument += string.Format(CultureInfo.InvariantCulture, " {0}:\"{1}\"", Program.ArgumentNameAccount, accountName);
            }
            return uploadArgument;
        }

        #endregion

        #region CommandLineActions Enum

        [Flags]
        private enum CommandLineActions
        {
            None = 0,
            UploadBackground = 1,
            UploadUI = 2,
            Add = 4,
            AddBatch = 8
        }

        #endregion
    }
}